Lancet

Lancet 引入

什么是 Lancet?

Lancet 是一个轻量级 Android AOP 框架,基于 ASM

  1. 编译速度快, 并且支持增量编译.
  2. 简洁的 API, 几行 Java 代码完成注入需求.
  3. 没有任何多余代码插入 apk.
  4. 支持用于 SDK, 可以在 SDK 编写注入代码来修改依赖 SDK 的 App.

lancet 引入

lancet 升级ASM9.1 , 适配Gradle7.5.1 AGP7.3.0 ,调整支持Java11

Gradle

// top gradle.build.kts
buildscript {
    dependencies {
        //noinspection UseTomlInstead
        classpath("me.ele:lancet-plugin:2.0.0")
    }
}

// app gradle.build.kts
plugins {
    id("me.ele.lancet")
}

和 AspectJ 对比

Lancet 本质上是 Gradle Plugin,通过依赖 Android 的打包插件提供的 Transform API,在打包过程中获取到所有的代码。
依赖 ASM 提供的字节码注入能力,通过我们解析自定义的注解,在目标点注入相应的代码。
通过注解进行 AOP 这点和 AspectJ 很相似,但是更加轻量和简洁,使用方式也有所不同。

这里要区分一个概念,编译期注入和运行期注入。

编译期:即在编译时对字节码做插桩修改,达到 AOP 的目的。优点是运行时无额外的性能损耗,但因为编译时的限制,只能修改最终打包到 APK 中的代码,即 Android Framework 的代码是固化在 ROM 中的,无法修改。
运行期:是只在运行时动态的修改代码的执行,因而可以修改 Framework 中代码的执行流程,在 hook 点上执行性能上有所损耗。

Android 没有 JVM 的环境,实际上 class 还会继续转换成 dex,因此 AspectJ 在 Android 平台是只能做到编译期注入。

使用

module 时不要 apply plugin: me.ele.lancet

代码织入方式

@Proxy

@Proxy 介绍

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Proxy {
    String value();
}

将使用新的方法替换代码里存在的原有的目标方法
应用场景:通常用于对系统 API 的劫持。因为虽然我们不能注入代码到系统提供的库之中,但我们可以劫持掉所有调用系统 API 的地方。

@Proxy 示例

示例:在 Log.d 输出,增加当前调用者的线程信息

@Proxy("d")
@TargetClass("android.util.Log")
public static int anyName(String tag, String msg) {
    tag = "hacket123." + tag;
    msg = msg + "-->>" + Thread.currentThread().getName();
    return (int) Origin.call();
}

在 Activity 的 onCreate 调用 Log.d,

Log.d("hacket","onCreate。。。。。。")

输出:

onCreate。。。。。。-->>main

反编译后:在原有的 msg 后增加了线程信息,原有方法调用替换成 包.aop注解的方法

public final class MainActivity extends ComponentActivity {
    public static final int $stable = LiveLiterals$MainActivityKt.INSTANCE.m5476Int$classMainActivity();

    /* loaded from: classes4.dex */
    class _lancet {
        private _lancet() {
        }

        @Proxy("d")
        @TargetClass("android.util.Log")
        static int com_example_lancetdemos_lancet_LancetAop_anyName(String str, String str2) {
            SystemClock.elapsedRealtime();
            return Log.d("hacket123." + str, str2 + "-->>" + Thread.currentThread().getName());
        }
    }

    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        _lancet.com_example_lancetdemos_lancet_LancetAop_anyName(LiveLiterals$MainActivityKt.INSTANCE.m5477String$arg0$calld$funonCreate$classMainActivity(), LiveLiterals$MainActivityKt.INSTANCE.m5478String$arg1$calld$funonCreate$classMainActivity());
        androidx.activity.compose.ComponentActivity.setContent$default(this, null, ComposableSingletons$MainActivityKt.INSTANCE.m5473getLambda3$app_debug(), 1, null);
    }
}

@NameRegex

@NameRegex 用来限制范围操作的作用域. 仅用于 Proxy 模式中, 比如你只想代理掉某一个包名下所有的目标操作. 或者你在代理所有的网络请求时,不想代理掉自己发起的请求. 使用 NameRegex 对 @TargetClass , @ImplementedInterface 筛选出的 class 再进行一次匹配。

@Insert

@Insert 介绍

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Insert {
    String value();
    boolean mayCreateSuper() default false;
}

@Insert 介绍

示例:注入每一个 Activity 的 onStop 生命周期


@TargetClass(value = "android.support.v7.app.AppCompatActivity", scope = Scope.LEAF)
@Insert(value = "onStop", mayCreateSuper = true)
protected void onStop(){
    System.out.println("hello world");
    Origin.callVoid();
}
protected void onStop() {
    System.out.println("hello world");
    super.onStop();
}
public @interface TargetClass {
    String value();

    Scope scope() default Scope.SELF;
}

public @interface ImplementedInterface {

    String[] value();

    Scope scope() default Scope.SELF;
}

public enum Scope {
    SELF,
    DIRECT,
    ALL,
    LEAF
}

@TargetClass 通过类

@ImplementedInterface

ma3nn
示例:当我们使用@ImplementedInterface(value = "I", scope = …) 时, 目标类如下:

如何获取类的全名

  1. 右键类,Copy→Copy Reference
  2. this.getClass() 看日志输出
  3. javap 命令
    1. 进入 app/build/intermediates/javac/debug/classes/com/example/lancetdemos
    2. javap -c MyRunnable
  4. 三方库,用 jadx-gui,上面会注释内部类的全路径名

ylsva
lgfj5

匿名内部类 com.appsflyer.internal.AFd1pSDK.4�是不对的,正确的应该是 com.appsflyer.internal.AFd1pSDK$4

  1. ClassyShark

java -jar $HACK_HOME/ClassyShark.jar -open

虽然在 Proxy , Insert 中我们指定了方法名, 但识别方法必须要更细致的信息. 我们会直接使用 Hook 方法的修饰符,参数类型来匹配方法.
所以一定要保持 Hook 方法的 public/protected/private static 信息与目标方法一致,参数类型,返回类型与目标方法一致.
返回类型可以用 Object 代替.
方法名不限. 异常声明也不限.

@ClassOf

没有权限声明目标类,用@ClassOf 注解来替代对类的直接 import。
ClassOf 的 value 一定要按照 (package_name.)(outer_class_name$)inner_class_name([]…) 的模板。
比如:

示例:

public class A {
    protected int execute(B b){
        return b.call();
    }

    private class B {

        int call() {
            return 0;
        }
    }
}

@TargetClass("com.dieyidezui.demo.A")
@Insert("execute")
public int hookExecute(@ClassOf("com.dieyidezui.demo.A$B") Object o) {
    System.out.println(o);
    return (int) Origin.call();
}

API

Origin

Origin 用来调用原目标方法,可以被多次调用。

如果你有捕捉异常的需求,可以使用:

This

仅用于 @Insert 方式的非静态方法的 Hook 中,否则报错

zvszb

  1. get() 返回目标方法被调用的实例化对象
  2. putField & getField

你可以直接存取目标类的所有属性,无论是 protected or private.
另外,如果这个属性不存在,我们还会自动创建这个属性. Exciting!
自动装箱拆箱肯定也支持了.
注意:

  1. Proxy 不能使用 This
  2. 你不能存取你父类的属性. 当你尝试存取父类属性时,我们还是会创建新的属性
package me.ele;
public class Main {
    private int a = 1;

    public void nothing(){

    }

    public int getA(){
        return a;
    }
}

@TargetClass("me.ele.Main")
@Insert("nothing")
public void testThis() {
    Log.e("debug", This.get().getClass().getName());
    This.putField(3, "a");
    Origin.callVoid();
}

示例

匿名内部类

public class StaticClassTest {

    public static class InnerClass {
        public int i;
        public void test() {
            System.out.println("InnerClass test");
        }
    }
}

hook:

@Insert("test")
@TargetClass(value = "com.example.lancetdemos.StaticClassTest$InnerClass", scope = Scope.SELF)
public void test() {
    StaticClassTest.InnerClass innerClass = (StaticClassTest.InnerClass) This.get();
    innerClass.i = 100;
    System.out.println("StaticClassTest.InnerClass test aop i=" + innerClass.i);
    Origin.callVoid();
}

反编译:
uzsmi

捕获 MyRunnable 崩溃

原始代码:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        Log.w("hacket", "MyRunnable run...");

        try {
            Thread.sleep(3000);
            Log.d("hacket", "MyRunnable run sleep 3 end...");
            int i = 1 / 0;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

hook:

@TargetClass(value = "com.example.lancetdemos.MyRunnable", scope = Scope.SELF)
@Insert(value = "run", mayCreateSuper = false)
protected void run() {
    Log.d("hacket", "MyRunnable aop run.");
    try {
        Origin.callVoid();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

用 jadx-gui 反编译看看:
8n3tl

方法内的方法捕获

原代码:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        Log.w("hacket", "MyRunnable run...");

        try {
            Thread.sleep(3000);
            Log.d("hacket", "MyRunnable run sleep 3 end...");
//            int i = 1 / 0;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        class InnerRunnable implements Runnable {
            @Override
            public void run() {
                Log.i("hacket", "InnerRunnable run start. class=" + this.getClass());
                String s = null;
                s.toLowerCase();

                Log.d("hacket", "InnerRunnable run end...");
            }
        }

        InnerRunnable innerRunnable = new InnerRunnable();
        innerRunnable.run();
    }
}

hook 代码:

@TargetClass(value = "com.example.lancetdemos.MyRunnable$1InnerRunnable", scope = Scope.SELF)
@Insert(value = "run", mayCreateSuper = false)
protected void run() {
    Log.d("hacket", "MyRunnable$InnerRunnable aop run add try catch.");
    try {
        Origin.callVoid();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

callThrowOne 示例

// 原始
public abstract class InputStream implements Closeable {
	public abstract int read() throws IOException;
}

@TargetClass("java.io.InputStream")
@Proxy("read")
public int read(byte[] bytes) throws IOException {
    try {
        return (int) Origin.<IOException>callThrowOne();
    } catch (IOException e) {
        e.printStackTrace();
        throw e;
    }
}

修复三方 sdk 的 crash

appsflyer 崩溃

2w3pd
hook 代码:

@TargetClass(value = "com.appsflyer.internal.AFd1pSDK$4", scope = Scope.SELF)
    @Insert(value = "run", mayCreateSuper = false)
    protected void run1() {
        Log.d("hacket", "com.appsflyer.internal.AFd1pSDK.4 aop run add try catch.");
        try {
            Origin.callVoid();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

hook 前代码:
tc4rn
hook 后代码:
xk4ko

插桩 OkHttp 添加 Flipper 的 Interceptor

public class OkHttpHook {

    // hook okhttp,添加一个拦截器

    @Insert("build")
    @TargetClass("okhttp3.OkHttpClient$Builder")
    public OkHttpClient hookBuild() {
        System.out.println("hook okhttp");
        OkHttpClient.Builder builder = (OkHttpClient.Builder) This.get();

        builder.addNetworkInterceptor(FlipperTool.getFlipperOkhttpInterceptor());

        OkHttpClient client = (OkHttpClient) Origin.call();
        return client;
    }
}

修复案例

public class CrashHook {

    //捕捉ConnectionTracker中unbindService出现异常的情况
    @Insert("unbindService")
    @TargetClass("com.google.android.gms.common.stats.ConnectionTracker")
    public void changeUnbindService(Context var1, ServiceConnection var2) { //getGoogleAnalytics
        try {
            Origin.callVoid();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    //捕捉RecyclerView动画崩溃问题,
    @TargetClass("androidx.recyclerview.widget.SimpleItemAnimator")
    @Insert("dispatchChangeFinished")
    public void changeUnbindService(RecyclerView.ViewHolder item, boolean oldItem) { //getGoogleAnalytics
        try {
            Origin.callVoid();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

//    @TargetClass("com.alibaba.android.arouter.launcher._ARouter")
//    @Insert("startActivity") //可能是options出错了,去掉options重试
//    private void startRouterActivity(int requestCode,
//                                     Context currentContext,
//                                     Intent intent,
//                                     Postcard postcard,
//                                     NavigationCallback callback) {
//        if (requestCode >= 0) {  // Need start for result
//            if (currentContext instanceof Activity) {
//                try {
//                    ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
//                } catch (Throwable e) {
//                    ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, null);
//                    FirebaseCrashlyticsProxy.INSTANCE.recordException(e);
//                }
//            } else {
//                Log.w(Consts.TAG, "Must use [navigation(activity, ...)] to support [startActivityForResult]");
//            }
//        } else {
//            try {
//                ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
//            } catch (Throwable e) {
//                ActivityCompat.startActivity(currentContext, intent, null);
//                FirebaseCrashlyticsProxy.INSTANCE.recordException(e);
//            }
//        }
//        if ((-1 != postcard.getEnterAnim() && -1 != postcard.getExitAnim()) && currentContext instanceof Activity) {    // Old version.
//            try {
//                ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
//            } catch (Throwable e) {
//                e.printStackTrace();
//                FirebaseCrashlyticsProxy.INSTANCE.recordException(e);
//            }
//        }
//        if (null != callback) { // Navigation over.
//            callback.onArrival(postcard);
//        }
//    }


    @TargetClass("okio.RealBufferedSink")
    @Insert("flush") //可能是options出错了,去掉options重试
    public void okioFlush() throws IOException {
        try {
            Origin.callVoid();
        } catch (Throwable e) {
            if (e instanceof IOException) {
                throw e;
            } else {
                throw new IOException(e);
            }
        }
    }


    //线上Google play日志,数据库打开有少量报错,firebase日志记录分析原因
//    @Insert("getWritableDatabase")
//    @TargetClass("androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper")
//    public SupportSQLiteDatabase getWritableDatabase() {
//        try {
//            return (SupportSQLiteDatabase) Origin.call();
//        } catch (Throwable e) {
//            try {
//                FirebaseCrashlyticsProxy.INSTANCE.recordException(e);
//            } catch (Throwable ignore) {
//            }
//            throw e;
//        }
//    }

    @TargetClass("com.facebook.animated.giflite.draw.MovieFrame")
    public void renderGifFrame(int w, int h, Bitmap bitmap) {
        try {
            Origin.callVoid();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    @Insert("setAlarm")
    @TargetClass("androidx.work.impl.utils.ForceStopRunnable")
    static void setAlarmFix(Context context) {
        try {
            Origin.callVoid();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    @Insert("updateScrollEventValues")
    @TargetClass("androidx.viewpager2.widget.ScrollEventAdapter")
    private void fixUpdateScrollEvent() {
        try {
            Origin.callVoid();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    @Insert("closeQuietly")
    @TargetClass("okhttp3.internal.Util")
    public static void fixCloseQuietly(Socket socket) {
        try {
            Origin.callVoid();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /*   @Insert("onActivityCreated")
       @TargetClass("com.google.firebase.messaging.FcmLifecycleCallbacks")
       public void fixFcmOnCreate(Activity var1, Bundle var2) {
           try {
               Origin.callVoid();
           } catch (Exception e) {
               try {
                   FirebaseCrashlyticsProxy.INSTANCE.recordException(e);
               } catch (Exception ignore) {
               }
               e.printStackTrace();
           }
       }*/
    //Fatal Exception: java.util.NoSuchElementException
//    @TargetClass("okhttp3.internal.connection.ConnectInterceptor")
//    public okhttp3.Response fixFindConnection(Interceptor.Chain chain) throws IOException {
//        try {
//            return (okhttp3.Response) Origin.call();
//        } catch (Throwable e) {
//            try {
//                FirebaseCrashlyticsProxy.INSTANCE.recordException(e);
//            } catch (Throwable ignore) {
//            }
//            if (!(e instanceof IOException)) {
//                throw new IOException(e);
//            } else {
//                throw e;
//            }
//        }
//    }

    @Insert("tryIntent")
    @TargetClass("com.facebook.login.NativeAppLoginMethodHandler")
    protected boolean fixTryIntent(Intent intent, int requestCode) {
        try {
            return (Boolean) Origin.call();
        } catch (Throwable e) {
            e.printStackTrace();
            return false;
        }
    }

    @Insert("prefetchPositionWithDeadline")
    @TargetClass("androidx.recyclerview.widget.GapWorker")
    private RecyclerView.ViewHolder fixRecyclerViewDeadline(RecyclerView view, int position, long deadlineNs) {
        try {
            return (RecyclerView.ViewHolder) Origin.call();
        } catch (Throwable e) {
            e.printStackTrace();
            return null; //返回null,reclerView某些情况也会出错崩溃
        }
    }

    //临时修复
    @Insert("dispatchStop")
    @TargetClass("androidx.fragment.app.FragmentController")
    public void fixFragmentDispatchStop() {
        try {
            Origin.callVoid();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @Insert("isForceStopped")
    @TargetClass("androidx.work.impl.utils.ForceStopRunnable")
    public boolean fixForceStopped() {
        try {
            return (Boolean) Origin.call();
        } catch (Throwable e) {
            return false;
        }
    }

    @TargetClass(value = "com.facebook.login.LoginFragment")
    @Insert(value = "onActivityResult")
    public void fixFacebookLoginResult(int requestCode, int resultCode, Intent data) {
        try {
            Origin.callVoid();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }


    @TargetClass(value = "com.google.android.material.resources.TypefaceUtils")
    @Insert(value = "maybeCopyWithFontWeightAdjustment")
    public static Typeface fixTypefaceCrash(android.content.res.Configuration configuration, Typeface typeface) {
        try {
            return (Typeface) Origin.call();
        } catch (Throwable e) {
            return null;
        }
    }

//    @TargetClass("com.huawei.hms.push.HmsMessageService")
//    @Insert("handleIntentMessage")
//    private void dispatchMessageHuaWei(Intent intent) {
//        Log.w("aws_push", "aws_push HmsMessageService dispatchMessage1");
//        Origin.callVoid();
//        NotifyReport.dispatchMessageT(intent);
//        Log.w("aws_push", "aws_push HmsMessageService dispatchMessage2");
//    }


    @TargetClass("com.tencent.mmkv.MMKV")
    @Insert("getAll")
    public Map<String, ?> catchMMKVGetAll() {
        try {
            return (Map<String, ?>) Origin.call();
        } catch (Throwable e) {
            e.printStackTrace();
            return new HashMap<>();
        }
    }

    @TargetClass("com.tencent.mmkv.MMKV")
    @Insert("registerOnSharedPreferenceChangeListener")
    public void catchMMKVRegisterOnSharedPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener) {
        try {
            Origin.callVoid();
        } catch (Throwable e) {
            if (BuildConfig.DEBUG) {
                e.printStackTrace();
            }
        }
    }

    @TargetClass("com.tencent.mmkv.MMKV")
    @Insert("unregisterOnSharedPreferenceChangeListener")
    public void catchMMKVUnregisterOnSharedPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener) {
        try {
            Origin.callVoid();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }


    //启动页res/drawable/abc_vector_test.xml 图片加载失败,导致启动页崩溃
    @Insert("getDrawableIfKnown")
    @TargetClass("androidx.appcompat.widget.TintTypedArray")
    public Drawable fixVectorCrash(int index) {
        try {
            return (Drawable) Origin.call();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    //商详页大图模块FragmentStateAdapter onStateChanged导致的崩溃
    @Insert("consumeRestoredStateForKey")
    @TargetClass("androidx.savedstate.SavedStateRegistry")
    public Bundle consumeRestoredStateForKey(String key) {
        if (BuildConfig.DEBUG) {
            return (Bundle) Origin.call();
        } else {
            try {
                return (Bundle) Origin.call();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    //商详/拍照等页面大图片会Crash
    @Insert("draw")
    @TargetClass("com.facebook.drawee.drawable.ForwardingDrawable")
    public void draw(Canvas canvas) {
        boolean check = true;
        try {
            Object o = This.get();
            check = ForwardingDrawableCheckUtils.check(o);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (check) {
            Origin.callVoid();
        }
    }

    //购物车刷新推荐列表 mChildHelper为空导致firebase少量崩溃
    @Insert("removeView")
    @TargetClass("androidx.recyclerview.widget.RecyclerView$LayoutManager")
    public void removeView(View child) {
        try {
            Origin.callVoid();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 列表滑动回收的时候,抛异常,捕获崩溃
    @Insert("recycleViewHolderInternal")
    @TargetClass("androidx.recyclerview.widget.RecyclerView$Recycler")
    public void recycleViewHolderInternal(RecyclerView.ViewHolder holder) {
        try {
            Origin.callVoid();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /*
     *
     * fix appsflyer sdk 6.10.3
     */
    @TargetClass(value = "com.appsflyer.internal.AFd1pSDK$4", scope = Scope.SELF)
    @Insert(value = "run")
    public void run() {
        try {
            Origin.callVoid();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意坑

Module requires ASM6

版本太旧

官方的版本太旧没维护了,不支持 7.x+ 高版本的;AGP8.0 不支持

需要自己 fork 维护,特别是 AGP8.0 后,Transformer API 移除了

不支持类的构造方法 hook

@Proxy("<init>")
@TargetClass("com.example.lancetdemos.MyOkhttpClient")
void OkHttpClient(MyOkhttpClient.Builder builder) {
    builder.i = 10024;
    Origin.callVoid();
}

利用 ByteX 的 lancet

https://github.com/Knight-ZXW/LancetX